북마크 이슈로 시작된 효율적인 세션 관리로 Next.js 성능 개선

2025-01-04

최근 Mocus 작업 중에 북마크 기능 추가중에 세션 상태에 관해 이슈가 발생했습니다.

기존 방식

  • 클라이언트와 서버 측에서 필요에 따라 세션 검사
  • 컴포넌트에서 세션 정보가 필요할 때마다 매번 세션 호출 후 사용
  • 이로 인한 중복 API 호출 증가 및 성능 저하

변경 이유

기존에 Supabase Authentication을 사용해 세션 관리를 하고 있었으나, 초기에는 클라이언트에서 세션 상태를 유지하지 않고 사용자 정보가 필요할 때마다 Supabase에서 인증 정보를 매번 가져오는 구조였습니다.

MVP 개발에 초점을 맞추다 보니 세션 관리 최적화가 미뤄졌었지만, 목업 아이템 리스트에서 문제가 두드러졌습니다. 세션 정보와 북마크 여부를 조회하는 API를 공용으로 사용되는 북마크 컴포넌트 내부에서 사용하고 있었기 때문에 당연하게도 렌더링 시점에 각 아이템마다 별도의 세션 정보를 요청하면서 리스트 아이템 수에 비례하여 API 호출이 증가했습니다.

예를 들어, 10개의 아이템을 렌더링할 때

  • Mockup Item 1 - Bookmark (세션 정보 호출)
  • Mockup Item 2 - Bookmark (세션 정보 호출)
  • ...

동일한 세션 정보를 여러 번 요청하는 비효율적인 패턴이 발생했습니다. Next.js의 클라이언트 서버에 대한 내부 요청이라 하더라도 불필요한 API 호출이 발생했습니다.

개선된 방식

민감하지 않은 세션 정보를 앱 초기화 시 한 번만 fetch하고 Zustand를 사용해 전역 상태로 SessionStore 관리하도록 변경했습니다.

  • 불필요한 반복 세션 호출 제거
  • 간결한 전역 상태 관리 패턴 구현
  • 클라이언트 성능과 서버 부하 개선
  • 컴포넌트 간 세션 데이터 일관성 유지

필요한 작업

  • 세션 스토어 추가
  • 앱 초기화시 supabase에서 최초 1회 기존에 세션 정보가 있다면 세션 정보 호출

Zustand SessionStore 추가

// session-store.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import supabase from '../supabase/client';

interface Session {
	id: string;
	email?: string;
	name?: string;
	avatar_url?: string;
	expires?: string;
}

interface SessionState {
	session: Session | null;
	isLoading: boolean;
	error: Error | null;
	setSession: (session: Session | null) => void;
	clearSession: () => void;
	fetchSession: () => Promise<void>;
}

  
export const useSessionStore = create<SessionState>()(
	devtools(
		persist(
			(set) => ({
				session: null,
				isLoading: false,
				error: null,
				setSession: (session) => set({ session }),
				clearSession: () => set({ session: null }),
				fetchSession: async () => {
					try {
						set({ isLoading: true, error: null });
						  
						const {
							data: { user },
						} = await supabase.auth.getUser();
						
						if (!user) {
							set({ session: null, isLoading: false });
							return;
						}
						 
						const session = {
							id: user?.id,
							email: user?.email,
							name: user?.user_metadata.name,
							avatar_url: user?.user_metadata.avatar_url,
						};
						
						set({ session, isLoading: false });
				
					} catch (error) {
						set({ error: error as Error, isLoading: false });
						console.error('Error fetching session:', error);
					}
				},
			}),
			{
				name: 'session-storage',
				partialize: (state) => ({ session: state.session }),
			},
		),
	),
);

useSessionStore를 통한 전역 관리 상태를 구성

앱 초기화 시 세션정보 호출

// client-layout.tsx
'use client';

import { useEffect } from 'react';
import { useSessionStore } from './_libs/zustand/session-store';

export default function ClientLayout({ children }: { children: React.ReactNode }) {
	const fetchSession = useSessionStore(state => state.fetchSession);

	useEffect(() => {
		fetchSession();
	}, [fetchSession]);
	
	return <>{children}</>;
}

루트 layout에서는 서버 컴포넌트로 사용하기 때문에 클라이언트에서 훅을 사용하기 위한 클라이언트용 레이아웃을 별도로 구성했습니다.

간단한 형태로 API 호출을 크게 줄이고 앱 성능을 향상시켰습니다.

  • 기존 방식
  • 변경 이유
  • 개선된 방식
    • 필요한 작업
    • Zustand SessionStore 추가
    • 앱 초기화 시 세션정보 호출